﻿namespace SpikingAnalyticsFrameLib

open System.IO
open SpikingNeuronLib
open SpikingAnalyticsLib
open SpikingAnalyticsLib.GeneralUtilities
open Deedle

[<AbstractClass; Sealed>]
type PNGDescriptorFrame () =

    /// Read the specified PNG descriptor file into a Frame
    /// Output frame format: ["t1"; "n1"; "t2"; "n2"; "weight"; "probability";]
    static member ReadDescriptorFile(descriptorFilePath, maxTime) =

        let data =
            File.ReadLines(descriptorFilePath)
            |> Seq.mapi (fun index line ->
                    if index = 0 then   // header
                        let minX, maxX = IOUtilities.ParseStringToTuple<int * int>(line)
                        minX, maxX, 0, 0, 0.0, 0.0
                    else
                        // e.g. 27 521 39 761 1.364000 1.000000
                        let t1, n1, t2, n2, weight, probability = IOUtilities.ParseStringToTuple<int * int * int * int * float * float>(line)
                        t1, n1, t2, n2, weight, probability
                )
            |> Seq.toList

        let minX, maxX =
            let minX, maxX, _, _, _, _ = data.Head
            minX, if maxTime > 0 then maxTime else maxX

        let plotData =
            data.Tail
            |> Frame.ofRecords
            |> Frame.indexColsWith(["t1"; "n1"; "t2"; "n2"; "weight"; "probability";])

        minX, maxX, plotData

    /// Generate a PNGDescriptor Frame from a list of connections that are consistently activated by a stimulus
    /// by using the firing event offset data from the specified response fingerprint frame
    /// and excluding connections containing neurons not included in includeSet
    /// Output frame format: ["t1"; "n1"; "t2"; "n2"; "weight"; "probability";]
    static member GeneratePNGDescriptor(frame:SpikingAnalyticsLib.Frame, includedNeurons:int list, jitter, activatedConnections:Connection list) =

        let descriptor =
            let offsetMap = frame.OffsetMap
            let includeSet = Set.ofList includedNeurons
            let delayOffsets = [ for i in 0..jitter -> i ]

            let linkedEvents =
                activatedConnections
                |> Seq.filter (fun connection -> includeSet.Contains connection.PreNeuron && includeSet.Contains connection.PostNeuron)
                |> Seq.mapi (fun index connection ->
//                    let delaySet =
//                        let delay = int connection.Delay
//                        [ delay - 2; delay - 1; delay; delay + 1; delay + 2; ]
//                        |> Seq.filter (fun delay -> delay >= 0)
//                        |> Set.ofSeq
                    let delaySet =
                        let delay = int connection.Delay
                        // to be causative, the impulse must arrive before (or at the same time as) the post-synaptic firing event
                        // delay; delay + 1; delay + 2; ...
                        delayOffsets
                        |> List.map (fun offset -> delay + offset)
                        |> Set.ofList
                    index, connection.PreNeuron, connection.PostNeuron, connection.Weight, delaySet
                )
                |> Seq.map (fun (index, preNeuron, postNeuron, weight, delays) ->
                    if offsetMap.ContainsKey preNeuron && offsetMap.ContainsKey postNeuron then
                        seq {
                            for preFiringTime in offsetMap.[preNeuron] do
                                for postFiringTime in offsetMap.[postNeuron] do
                                    yield preFiringTime, postFiringTime
                        }
                        |> Seq.filter (fun (preFiringTime, postFiringTime) -> delays.Contains (postFiringTime - preFiringTime))
                        // LinkedEventPair: a pair of linked firing events of the form ((preTime, preNeuron), (postTime, postNeuron), connectionWeight, probability)
                        |> Seq.map (fun (preFiringTime, postFiringTime) -> (preFiringTime, preNeuron), (postFiringTime, postNeuron), weight, 0.0)
                    else
                        Seq.empty
                )
                |> Seq.concat
                |> Seq.cast<LinkedEventPair>
                |> Seq.toList

            new PNGDescriptor(linkedEvents)

        descriptor.LinkedEventsAsTuples
        |> Seq.map (fun ((preTime, preNeuron), (postTime, postNeuron), connectionWeight, probability) ->
                // unwrap each LinkedEventPair
                preTime, preNeuron, postTime, postNeuron, connectionWeight, probability
            )
        |> Frame.ofRecords
        |> Frame.indexColsWith(["t1"; "n1"; "t2"; "n2"; "weight"; "probability";])

    /// Generate a descriptor Frame from a PNGDescriptor
    static member GetFrame(groupDescriptor:PNGDescriptor) =
        PNGDescriptorFrame.GetFrame(groupDescriptor.LinkedEventsAsTuples)

    /// Generate a descriptor Frame from a PNGDescriptor, removing links between neurons greater than or equal to the specified Y limit
    static member GetFrame(groupDescriptor:PNGDescriptor, maxNeuron) =
        let limitedLinks =
            groupDescriptor.LinkedEventsAsTuples
            |> Seq.filter (fun ((preTime, preNeuron), (postTime, postNeuron), connectionWeight, probability) -> (preNeuron < maxNeuron) && (postNeuron < maxNeuron))
        PNGDescriptorFrame.GetFrame(limitedLinks)

    // Generate a descriptor Frame from a sequence of linked event pairs
    static member GetFrame(linkedEventPairs:LinkedEventPair list) =
        let eventPairTuples =
            linkedEventPairs
            |> Seq.map (fun eventPair -> eventPair.AsTuple())
            |> Seq.map (fun (preEvent, postEvent, connectionWeight, connectionDelay, layer, probability) -> preEvent, postEvent, connectionWeight, probability)

        PNGDescriptorFrame.GetFrame(eventPairTuples)

    // Generate a descriptor Frame from a sequence of linked event pair tuples
    static member GetFrame(linkedEventPairs:seq<(int * int) * (int * int) * float * float>) =
        linkedEventPairs
        |> Seq.map (fun ((preTime, preNeuron), (postTime, postNeuron), connectionWeight, probability) ->
                // unwrap each LinkedEventPair
                preTime, preNeuron, postTime, postNeuron, connectionWeight, probability
            )
        |> Frame.ofRecords
        |> Frame.indexColsWith(["t1"; "n1"; "t2"; "n2"; "weight"; "probability";])

    /// Generate an overlay frame for the specified firing events using the specified color
    // Pass to PNGVisualisation.OverlayFiringEvents(overlayFrame, n) to overlay filled circles showing the firing events
    static member GetOverlayFrame(firingEvents:#seq<int * int>, color) =
        firingEvents
        |> Seq.map (fun (time, nindex) -> time, nindex, color)
        |> Frame.ofRecords
        |> Frame.indexColsWith(["time"; "nindex"; "color";])

    /// Generate a descriptor Frame from a PNGDescriptor whose neurons are partitioned by the specified partitioning schema
    /// The partitioning schema remaps the neuron index ordering so that neurons can be clustered together in the resulting Frame
    static member GetPartitionedFrame(descriptor:PNGDescriptor, clusteredMapping:Map<int, int>, maxNeuron) =
        let clusteredDescriptor = descriptor.Transpose(clusteredMapping)
        PNGDescriptorFrame.GetFrame(clusteredDescriptor, maxNeuron)
